Blob Detection

Jacky Baltes
National Taiwan Normal University
Taipei, Taiwan
jacky.baltes@ntnu.edu.tw

18 October 2022
image = Image.open( str(img1.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
print(marker1.shape)

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow(marker1)
(480, 640, 4)
top_left = (int(297 * 640/575), int(170 * 480/436) ) 
bottom_right = (int(307 * 640/575), int(178 * 480/436) )

#top_left = [297, 170 ]
#bottom_right = [ 307, 178 ]
print(marker1.shape)
seed_coords = [ [ top_left[0], top_left[1] ], [ bottom_right[0], bottom_right[1] ] ]
print(seed_coords)

seed = marker1[ seed_coords[0][1]:seed_coords[1][1], seed_coords[0][0]:seed_coords[1][0] ]
r_min = np.min( seed[:,:,0 ] )
r_max = np.max( seed[:,:,0] )
g_min = np.min( seed[:,:,1 ] )
g_max = np.max( seed[:,:,1] )
b_min = np.min( seed[:,:,2 ] )
b_max = np.max( seed[:,:,2] )

print("r_min", r_min, "r_max", r_max, "g_min", g_min, "g_max", g_max, "b_min", b_min, "b_max", b_max )
(480, 640, 4)
[[330, 187], [341, 195]]
r_min 183 r_max 234 g_min 137 g_max 194 b_min 0 b_max 54
test = marker1.copy()

height, width, depth = test.shape
out1 = np.zeros((height, width, 3),dtype=np.uint8)

replaced = 0
for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x,0:3]
    if r >= r_min and r <= r_max and g >= g_min and g <= g_max and b >= b_min and b <= b_max:
      col = [ 0, 0, 255 ]
      replaced = replaced + 1
    else:
      col = [ r, g, b ]
    out1[y,x] = col

print( f'replaced {replaced} pixels' )
replaced 2337 pixels
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow(out1)
seed_coords = [ [ top_left[0], top_left[1] ], [ bottom_right[0], bottom_right[1] ] ]
print(seed_coords)
seed = marker1[ seed_coords[0][1]:seed_coords[1][1], seed_coords[0][0]:seed_coords[1][0] ]
r_min = np.min( seed[:,:,0 ] )
r_max = np.max( seed[:,:,0] )
g_min = np.min( seed[:,:,1 ] )
g_max = np.max( seed[:,:,1] )
b_min = np.min( seed[:,:,2 ] )
b_max = np.max( seed[:,:,2] )

print("r_min", r_min, "r_max", r_max, "g_min", g_min, "g_max", g_max, "b_min", b_min, "b_max", b_max )

test = marker1.copy()[:,:,0:3]

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x]
    if r >= r_min and r <= r_max and g >= g_min and g <= g_max and b >= b_min and b <= b_max:
      test[y,x] = [ 0, 0, 255 ]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.plot( [ seed_coords[0][0], seed_coords[1][0], seed_coords[1][0], seed_coords[0][0], seed_coords[0][0] 
           ], 
        [ seed_coords[0][1], seed_coords[0][1], seed_coords[1][1], seed_coords[1][1], seed_coords[0][1]
           ],
         'r-')

# ax.plot( [ seedCoords[0][0], seedCoords[1][0], 
#           seedCoords[1][0], seedCoords[1][0], seedCoords[0][0] ], 
#         [ seedCoords[0][1], seedCoords[1][1], 
#           seedCoords[1][1], seedCoords[1][1], seedCoords[0][1] ],
#          'r-')

ax.imshow(test)
[[330, 187], [341, 195]]
r_min 183 r_max 234 g_min 137 g_max 194 b_min 0 b_max 54
top_left = (int(275 * 640/575), int(190 * 480/436) ) 
bottom_right = (int(305 * 640/575), int(230 * 480/436) )

seed_coords = [ [ top_left[0], top_left[1] ], [ bottom_right[0], bottom_right[1] ] ]
print(seed_coords)
seed = marker1[ seed_coords[0][1]:seed_coords[1][1], seed_coords[0][0]:seed_coords[1][0] ]
r_min = np.min( seed[:,:,0 ] )
r_max = np.max( seed[:,:,0] )
g_min = np.min( seed[:,:,1 ] )
g_max = np.max( seed[:,:,1] )
b_min = np.min( seed[:,:,2 ] )
b_max = np.max( seed[:,:,2] )

print("r_min", r_min, "r_max", r_max, "g_min", g_min, "g_max", g_max, "b_min", b_min, "b_max", b_max )

test = marker1.copy()[:,:,0:3]

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x]
    if r >= r_min and r <= r_max and g >= g_min and g <= g_max and b >= b_min and b <= b_max:
      test[y,x] = [ 0, 0, 255 ]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.plot( [ seed_coords[0][0], seed_coords[1][0], seed_coords[1][0], seed_coords[0][0], seed_coords[0][0] 
           ], 
        [ seed_coords[0][1], seed_coords[0][1], seed_coords[1][1], seed_coords[1][1], seed_coords[0][1]
           ],
         'r-')

# ax.plot( [ seedCoords[0][0], seedCoords[1][0], 
#           seedCoords[1][0], seedCoords[1][0], seedCoords[0][0] ], 
#         [ seedCoords[0][1], seedCoords[1][1], 
#           seedCoords[1][1], seedCoords[1][1], seedCoords[0][1] ],
#          'r-')

ax.imshow(test)
[[306, 209], [339, 253]]
r_min 1 r_max 154 g_min 106 g_max 196 b_min 109 b_max 218

Color Detection

Color detection is the easiest way to try and detect objects in robot and computer vision applications

Initial research in intelligent robotics used lots of color coded environments. E.g., RoboCup Humanoid League - red ball, blue goal, yellow goal until 2012, nowadays ball and goals are all white

.

There are two types of errors in any (vision) system: False positives and false negatives. Tuning the detection to find the right balance between them.

ball2 = addJBImage( name="ball2", height=0, width=0, url="https://i.postimg.cc/T3BsXzr7/ball2.jpg" )
image = Image.open( str(ball2.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
print(marker1.shape)

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow(marker1)
(480, 640, 3)
seedCoords = [ [ 10, 240 ], [ 30, 265 ] ]
seed = marker1[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]
rMin = np.min( seed[:,:,0 ] )
rMax = np.max( seed[:,:,0] )
gMin = np.min( seed[:,:,1 ] )
gMax = np.max( seed[:,:,1] )
bMin = np.min( seed[:,:,2 ] )
bMax = np.max( seed[:,:,2] )

print("rMin", rMin, "rMax", rMax, "gMin", gMin, "gMax", gMax, "bMin", bMin, "bMax", bMax )

test = marker1.copy()

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x]
    if r >= rMin and r <= rMax and g >= gMin and g <= gMax and b >= bMin and b <= bMax:
      test[y,x] = [ 0, 0, 255 ]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.plot( [ seedCoords[0][0], seedCoords[1][0], seedCoords[1][0], seedCoords[0][0], seedCoords[0][0] 
           ], 
        [ seedCoords[0][1], seedCoords[0][1], seedCoords[1][1], seedCoords[1][1], seedCoords[0][1]
           ],
         'r-')

# ax.plot( [ seedCoords[0][0], seedCoords[1][0], 
#           seedCoords[1][0], seedCoords[1][0], seedCoords[0][0] ], 
#         [ seedCoords[0][1], seedCoords[1][1], 
#           seedCoords[1][1], seedCoords[1][1], seedCoords[0][1] ],
#          'r-')

ax.imshow(test)
rMin 172 rMax 240 gMin 151 gMax 247 bMin 32 bMax 176

Normalized RGB Color Space

  • RGB color space is susceptible to changes in illumination
  • Many other color spaces have been used HSV, YUV, Cosine transform, ...
  • On average, none of them work very well
  • Another improved color space is normalized RGB, where we normalize the RGB vector
  • \[ r_n = \frac{r}{r+g+b+1}, g_n = \frac{g}{r+g+b+1}, b_n = \frac{b}{r+g+b+1} \]
ball1 = ball2

image = Image.open( str(ball1.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
rgbNorm = marker1.copy()

height, width, depth = rgbNorm.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = rgbNorm[y,x]
    sum = int(r) + int(g) + int(b) + 1   # Add 1 to avoid possible divide by 0
    p = [ 255 * r / sum, 255 * g / sum, 255 * b / sum ]
    # if ( p[0] >= 1.0 ) or ( p[1] >= 1.0 ) or ( p[2] >= 1.0 ):
    #   print( r, g, b, sum, p )
    rgbNorm[y,x] = p

seedCoords = [ [ 180, 170 ], [ 190, 180 ] ]
seed = rgbNorm[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]

rMin = np.min( seed[:,:,0 ] )
rMax = np.max( seed[:,:,0] )
gMin = np.min( seed[:,:,1 ] )
gMax = np.max( seed[:,:,1] )
bMin = np.min( seed[:,:,2 ] )
bMax = np.max( seed[:,:,2] )

print("rMin", rMin, "rMax", rMax, "gMin", gMin, "gMax", gMax, "bMin", bMin, "bMax", bMax )

test = rgbNorm.copy()

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x]
    if r >= rMin and r < rMax and g >= gMin and g <= gMax and b >= bMin and b <= bMax:
      test[y,x] = [ 0, 0, 255 ]


fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,2,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow(rgbNorm)

ax2 = fig.add_subplot(1,2,2)

ax2.set_xticks([])
ax2.set_yticks([])

ax2.plot( [ seedCoords[0][0], seedCoords[1][0], seedCoords[1][0], seedCoords[0][0], seedCoords[0][0] 
           ], 
        [ seedCoords[0][1], seedCoords[0][1], seedCoords[1][1], seedCoords[1][1], seedCoords[0][1]
           ],
         'r-')
ax2.imshow(test)
rMin 96 rMax 116 gMin 94 gMax 105 bMin 34 bMax 64
image = Image.open( str(ball2.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
rgbNorm = marker1.copy()

height, width, depth = rgbNorm.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = rgbNorm[y,x]
    sum = int(r) + int(g) + int(b) + 1   # Add 1 to avoid possible divide by 0
    p = [ 255 * r / sum, 255 * g / sum, 255 * b / sum ]
    # if ( p[0] >= 1.0 ) or ( p[1] >= 1.0 ) or ( p[2] >= 1.0 ):
    #   print( r, g, b, sum, p )
    rgbNorm[y,x] = p

seedCoords = [ [ 10, 240 ], [ 30, 265 ] ]
seed = rgbNorm[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]

rMin = np.min( seed[:,:,0 ] )
rMax = np.max( seed[:,:,0] )
gMin = np.min( seed[:,:,1 ] )
gMax = np.max( seed[:,:,1] )
bMin = np.min( seed[:,:,2 ] )
bMax = np.max( seed[:,:,2] )

print("rMin", rMin, "rMax", rMax, "gMin", gMin, "gMax", gMax, "bMin", bMin, "bMax", bMax )

test = rgbNorm.copy()

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    r,g,b = test[y,x]
    if r >= rMin and r < rMax and g >= gMin and g <= gMax and b >= bMin and b <= bMax:
      test[y,x] = [ 0, 0, 255 ]


fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,2,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow(rgbNorm)

ax2 = fig.add_subplot(1,2,2)

ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow(test)
rMin 91 rMax 122 gMin 93 gMax 119 bMin 18 bMax 69

Color Difference Model

  • Extend the RGB color model by adding a red-green, red-blue, and green-blue channels
  • A pixel is 6 dimensional: r, g, b, r-g, r-b, g-b
  • This is an efficient color model, since r-g, r-b, g-b in one subtraction and AND and operation
image = Image.open( str(ball1.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)

cdiff = np.zeros((marker1.shape[0], marker1.shape[1], marker1.shape[2] * 2))
cdiff[:,:,0:3] = marker1
cdiff[:,:,3] = cdiff[:,:,0] - cdiff[:,:,1]  # red - green
cdiff[:,:,4] = cdiff[:,:,0] - cdiff[:,:,2]  # red - blue
cdiff[:,:,5] = cdiff[:,:,1] - cdiff[:,:,2]  # green - blue

height, width, depth = cdiff.shape
print(cdiff.shape)

seedCoords = [ [ 8, 238 ], [ 32, 267 ] ]    
seed = cdiff[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]

limits = np.zeros((2,6))
for i in range(6):
  limits[0,i] = np.min(seed[:,:,i])
  limits[1,i] = np.max(seed[:,:,i])

print( 'limits', limits )
test = marker1.copy()

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    fnd = True
    for i in range(6):
      if limits[0,i] > cdiff[y,x,i] or limits[1,i] < cdiff[y,x,i]:
        fnd = False
        break
    if fnd:
      test[y,x,:] = [0,0,255]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow( test )
(480, 640, 6)
limits [[163. 145.  32. -20.  44.  35.]
 [242. 247. 187.  23. 176. 178.]]
image = Image.open( str(ball2.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
cdiff = np.zeros((marker1.shape[0], marker1.shape[1], marker1.shape[2] * 2))
cdiff[:,:,0:3] = marker1
cdiff[:,:,3] = cdiff[:,:,0] - cdiff[:,:,1]  # red - green
cdiff[:,:,4] = cdiff[:,:,0] - cdiff[:,:,2]  # red - blue
cdiff[:,:,5] = cdiff[:,:,1] - cdiff[:,:,2]  # green - blue

height, width, depth = cdiff.shape
print(cdiff.shape)

seedCoords = [ [ 10, 240 ], [ 30, 265 ] ] 
seed = cdiff[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]

limits = np.zeros((2,6))
for i in range(6):
  limits[0,i] = np.min(seed[:,:,i])
  limits[1,i] = np.max(seed[:,:,i])

print( 'limits', limits )

test = marker1.copy()

height, width, depth = test.shape

for y in range(  height ):
  for x in range(  width ):
    fnd = True
    for i in range(6):
      if limits[0,i] > cdiff[y,x,i] or limits[1,i] < cdiff[y,x,i]:
        fnd = False
        break
    if fnd:
      test[y,x,:] = [0,0,255]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow( test )
(480, 640, 6)
limits [[172. 151.  32. -20.  59.  62.]
 [240. 247. 176.  23. 170. 178.]]
gradient = np.zeros( ( 480, 640, 3 ), dtype=int )

height, width, depth = gradient.shape
gradWidth = 10

for xi in range(  width//gradWidth ):
  gradient[0:height//3, xi * gradWidth: (xi + 1) * gradWidth ] = [ int( 255 * xi * 1/64 ), 0, 0 ]

for xi in range(  width//gradWidth ):
  gradient[height//3:2*height//3, xi * gradWidth: (xi + 1) * gradWidth ] = [ 0, int( 255 * xi * 1/64 ), 0 ]

for xi in range(  width//gradWidth ):
  gradient[2 * height//3:height, xi * gradWidth: (xi + 1) * gradWidth ] = [ 0, 0, int( 255 * xi * 1/64 ) ]

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow( gradient )
# I often cheat when converting to greyscale and just the green channel

green = marker1[:,:,1]
plt.imshow(green,cmap='gray')

Grouping Pixels to form Blobs

  • Must group pixels that belong together (adjacent to each other)
  • Adjacent could be above, below, right, left, above-left, above-right, below-left, below-right)
  • Create a secondary array of object ids flags
  • Iterate through image and check if pixels is target color
  • If pixels is the target color, then check if an object is adjacent
def find_blobs(image, rmin, rmax, gmin, gmax, bmin, bmax, size_lim = 200):
  flags = np.zeros(image.shape[:2],dtype=int)
  centers = []
  for x in range(1,image.shape[0]-1):
    for y in range(1,image.shape[1]-1):
      r, g, b = image[x,y]
      if r >= rmin and r <= rmax and g >= gmin and g <= gmax \
         and b >= bmin and b <= bmax:
        #print("x=", x-1, ":", x+2, "y=", y-1,":", y+2, flags[x-1:x+2,y-1:y+2])
        n = np.max( flags[x-1:x+2,y-1:y+2])
        if n == 0:
          index = len(centers)+1
          centers.append([x, y, 1])
        else:
          index = n
          centers[index-1][0] += x
          centers[index-1][1] += y
          centers[index-1][2] += 1
        flags[x,y] = index
  keep = []
  for center in centers:
    center[0] /= center[2]
    center[1] /= center[2]
    if center[2] > size_lim:
      keep.append(center)
  return keep

blobs = find_blobs(marker1, 225, 255, 0, 15, 0, 15)
print(blobs)
[]
def draw_cross(image, x, y, color):
  image[x-2:x+3,y-10:y+11] = color
  image[x-10:x+11,y-2:y+3] = color
image = Image.open( str(ball1.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image)
#seedCoords = [ [ 169, 139 ], [ 211, 234 ] ]
seedCoords = [ [ 10, 240 ], [ 30, 265 ] ] 
seed = marker1[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]
rMin = np.min( seed[:,:,0 ] )
rMax = np.max( seed[:,:,0] )
gMin = np.min( seed[:,:,1 ] )
gMax = np.max( seed[:,:,1] )
bMin = np.min( seed[:,:,2 ] )
bMax = np.max( seed[:,:,2] )

blobs = find_blobs(marker1, rMin, rMax, gMin, gMax, bMin, bMax, 200)
print(blobs)

test = marker1.copy()

for xc, yc, size in blobs:
  draw_cross( test, int(xc), int(yc), (255,0,255) )

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow( test )
[[31.03813559322034, 283.79237288135596, 236], [67.05592105263158, 526.3125, 304], [74.73214285714286, 452.14732142857144, 224], [78.47777777777777, 405.6277777777778, 360], [89.0327868852459, 305.12295081967216, 244], [127.58064516129032, 265.3727598566308, 279], [131.65420560747663, 377.88317757009344, 214], [132.4061135371179, 220.11353711790392, 229], [136.64, 278.78, 250], [139.12994350282486, 391.0734463276836, 354], [143.88694267515925, 428.2547770700637, 628], [153.73058252427185, 215.71844660194174, 412], [156.2233009708738, 181.51132686084142, 309], [167.93532338308458, 195.66169154228857, 201], [170.39236111111111, 159.03125, 288], [177.44913151364764, 170.15384615384616, 403], [177.21476510067114, 532.4161073825503, 298], [189.5, 550.1510416666666, 576], [189.2127659574468, 341.5045592705167, 329], [193.29850746268656, 352.9950248756219, 201], [233.08133971291866, 478.8899521531101, 836], [242.25974025974025, 10.535714285714286, 308], [244.01703163017032, 499.14355231143554, 411], [249.53608247422682, 18.966494845360824, 388], [257.5514705882353, 35.599264705882355, 544], [280.1531823085221, 233.26429341963322, 927], [272.4030769230769, 429.4123076923077, 325], [284.7324561403509, 247.75877192982455, 228], [294.8056872037915, 264.63744075829385, 422], [288.1616939364774, 437.6053897978826, 1039], [417.2461648234035, 326.0991794505887, 2803]]
image = Image.open( str(ball2.getDefaultFileName() ) )
image = image.resize((640,480))
marker1 = np.asarray(image).copy()
marker1[:,0:5,:] = 0
marker1[0:5,:,:] = 0

seedCoords = [ [ 10, 240 ], [ 30, 265 ] ] 
seed = marker1[ seedCoords[0][1]:seedCoords[1][1], seedCoords[0][0]:seedCoords[1][0] ]
rMin = np.min( seed[:,:,0 ] )
rMax = np.max( seed[:,:,0] )
gMin = np.min( seed[:,:,1 ] )
gMax = np.max( seed[:,:,1] )
bMin = np.min( seed[:,:,2 ] )
bMax = np.max( seed[:,:,2] )

blobs = find_blobs(marker1, rMin, rMax, gMin, gMax, bMin, bMax, 200)
print(blobs)

test = marker1.copy()

for xc, yc, size in blobs:
  draw_cross( test, int(xc), int(yc), (255,0,255) )

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(1,1,1)

ax.set_xticks([])
ax.set_yticks([])
ax.imshow( test )
[[31.03813559322034, 283.79237288135596, 236], [67.05592105263158, 526.3125, 304], [74.73214285714286, 452.14732142857144, 224], [78.47777777777777, 405.6277777777778, 360], [89.0327868852459, 305.12295081967216, 244], [127.58064516129032, 265.3727598566308, 279], [131.65420560747663, 377.88317757009344, 214], [132.4061135371179, 220.11353711790392, 229], [136.64, 278.78, 250], [139.12994350282486, 391.0734463276836, 354], [143.88694267515925, 428.2547770700637, 628], [153.73058252427185, 215.71844660194174, 412], [156.2233009708738, 181.51132686084142, 309], [167.93532338308458, 195.66169154228857, 201], [170.39236111111111, 159.03125, 288], [177.44913151364764, 170.15384615384616, 403], [177.21476510067114, 532.4161073825503, 298], [189.5, 550.1510416666666, 576], [189.2127659574468, 341.5045592705167, 329], [193.29850746268656, 352.9950248756219, 201], [233.08133971291866, 478.8899521531101, 836], [240.412, 12.388, 250], [244.01703163017032, 499.14355231143554, 411], [249.42297650130547, 19.16710182767624, 383], [257.5514705882353, 35.599264705882355, 544], [280.1531823085221, 233.26429341963322, 927], [272.4030769230769, 429.4123076923077, 325], [284.7324561403509, 247.75877192982455, 228], [294.8056872037915, 264.63744075829385, 422], [288.1616939364774, 437.6053897978826, 1039], [417.2461648234035, 326.0991794505887, 2803]]
image = np.array( [ 
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],                 
])

fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.imshow( image )

blobs = find_blobs( image, 200, 255, 0, 255, 0, 255, 1)
print(blobs)
[[3.1, 1.6, 10], [1.5, 4.5, 4]]
image = np.array( [ 
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 255, 0, 0, ], [ 255, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],
  [ [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ], [ 0, 0, 0, ] ],                 
])

fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.imshow( image )

blobs = find_blobs( image, 200, 255, 0, 255, 0, 255, 1)
print(blobs)
[[2.7777777777777777, 1.5555555555555556, 9], [3.5, 3.8, 10]]
def color_pred( pixel, color ):
    return ( ( pixel[0] >= color[0] ) and ( pixel[0] <= color[1] ) and
          ( pixel[1] >= color[2] ) and ( pixel[1] <= color[3] ) and
          ( pixel[2] >= color[4] ) and ( pixel[2] <= color[5] ) )
      
def flood_fill(image, x, y, color ):
  height, width, depth = image.shape
  stack = [ (y,x) ]
  image[y,x] = [0,0,0]
  sum_x, sum_y, size = 0, 0, 0

  while( len(stack) > 0 ):
    yp, xp = stack.pop()
    print("Processing pixel at ", yp, ",", xp, stack )
    for dy, dx in [ [ -1, 0], [1,0], [0,-1], [0,1], [-1,-1], [-1,1], [1,-1], [1,1] ]:
      yn,xn = yp+dy, xp+dx
      if ( (yn >= 0) and ( yn < height ) and ( xn >= 0 ) and ( xn < width ) ):
        if color_pred( image[yn, xn], color ):
          sum_x, sum_y, size = sum_x + xn, sum_y+yn, size+1
          stack.append( (yn, xn) )
          image[yn,xn] = [0,0,0]

  return ( sum_x/size, sum_y/size, size )

def search_objects( image, color ):
  height, width, channel = image.shape
  centers = []
  for y in range(height):
    for x in range(width):
      if color_pred(image[y,x], color):
        centers.append( flood_fill(image, x, y, color ) )
  return centers
img = image.copy()
blobs = search_objects( img, [ 200, 255, 0, 255, 0, 255 ] )
print( blobs )
Processing pixel at  1 , 1 []
Processing pixel at  2 , 2 [(2, 1), (1, 2)]
Processing pixel at  3 , 1 [(2, 1), (1, 2), (3, 2)]
Processing pixel at  4 , 2 [(2, 1), (1, 2), (3, 2), (4, 1)]
Processing pixel at  5 , 3 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2)]
Processing pixel at  6 , 2 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4), (4, 4)]
Processing pixel at  4 , 4 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4)]
Processing pixel at  3 , 4 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4)]
Processing pixel at  2 , 5 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4), (2, 4)]
Processing pixel at  1 , 4 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4), (2, 4), (1, 5)]
Processing pixel at  1 , 5 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4), (2, 4)]
Processing pixel at  2 , 4 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3), (5, 4)]
Processing pixel at  5 , 4 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2), (6, 3)]
Processing pixel at  6 , 3 [(2, 1), (1, 2), (3, 2), (4, 1), (5, 2)]
Processing pixel at  5 , 2 [(2, 1), (1, 2), (3, 2), (4, 1)]
Processing pixel at  4 , 1 [(2, 1), (1, 2), (3, 2)]
Processing pixel at  3 , 2 [(2, 1), (1, 2)]
Processing pixel at  1 , 2 [(2, 1)]
Processing pixel at  2 , 1 []
[(2.8333333333333335, 3.2777777777777777, 18)]